package org.apache.pivot.tools.wtkxbrowser;

import java.awt.Color;
import java.awt.Desktop;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.io.ByteArrayOutputStream;
import java.net.URISyntaxException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.io.FileUtils;
import org.apache.pivot.collections.ArrayList;
import org.apache.pivot.collections.HashMap;
import org.apache.pivot.collections.List;
import org.apache.pivot.collections.Map;
import org.apache.pivot.serialization.SerializationException;
import org.apache.pivot.wtk.Action;
import org.apache.pivot.wtk.Alert;
import org.apache.pivot.wtk.Application;
import org.apache.pivot.wtk.ApplicationContext;
import org.apache.pivot.wtk.Bounds;
import org.apache.pivot.wtk.BoxPane;
import org.apache.pivot.wtk.Button;
import org.apache.pivot.wtk.Component;
import org.apache.pivot.wtk.ComponentMouseButtonListener;
import org.apache.pivot.wtk.Container;
import org.apache.pivot.wtk.DesktopApplicationContext;
import org.apache.pivot.wtk.Display;
import org.apache.pivot.wtk.FileBrowserSheet;
import org.apache.pivot.wtk.ListView;
import org.apache.pivot.wtk.MessageType;
import org.apache.pivot.wtk.Mouse;
import org.apache.pivot.wtk.Orientation;
import org.apache.pivot.wtk.Prompt;
import org.apache.pivot.wtk.PushButton;
import org.apache.pivot.wtk.Sheet;
import org.apache.pivot.wtk.SheetCloseListener;
import org.apache.pivot.wtk.TabPane;
import org.apache.pivot.wtk.TabPaneSelectionListener;
import org.apache.pivot.wtk.TextArea;
import org.apache.pivot.wtk.TextInput;
import org.apache.pivot.wtk.Window;
import org.apache.pivot.wtk.content.ListViewItemRenderer;
import org.apache.pivot.wtk.effects.Decorator;
import org.apache.pivot.wtkx.WTKX;
import org.apache.pivot.wtkx.WTKXSerializer;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URI;
import java.util.Arrays;
import java.util.IdentityHashMap;
import org.apache.pivot.json.JSONSerializer;
import org.apache.pivot.wtk.ButtonPressListener;
import org.apache.pivot.wtk.LinkButton;

/**
 * @author dmitry.mamonov
 */
public class WtkxBrowser implements Application {
    Window window;
    @WTKX
    PushButton buttonAddWtkx;
    @WTKX
    PushButton buttonRemoveWtkx;
    @WTKX
    PushButton buttonMoveWtkxUp;
    @WTKX
    ListView listViewOfWtkxResources;
    @WTKX
    PushButton buttonAddTab;
    @WTKX
    PushButton buttonCloseTab;
    @WTKX
    TabPane tabPane;
    @WTKX
    Container header;

    @WTKX
    LinkButton linkFeedback;


    Model model = new Model();
    static ByteArrayOutputStream stderr = new ByteArrayOutputStream();


    FileBrowserSheet fileBrowserSheet = new FileBrowserSheet(FileBrowserSheet.Mode.OPEN_MULTIPLE) {
        {
            setRootDirectory(new File(".").getAbsoluteFile());
        }
    };

    public void startup(Display display, Map<String, String> properties) throws Exception {
        WTKXSerializer wtkx = new WTKXSerializer();
        window = (Window) wtkx.readObject(this, "WtkxBrowser.wtkx");
        window.setIcon(this.getClass().getResource("logo-16.png"));

        //process head components
        WTKXSerializer head = wtkx.getSerializer("head");
        head.bind(this, WtkxBrowser.class);
        initHeadComponents();

        //process body components
        WTKXSerializer body = wtkx.getSerializer("body");
        body.bind(this, WtkxBrowser.class);
        initBodyComponents();

        //add first tab
        model.load();
        if (tabPane.getTabs().getLength() == 0) {
            addTab(model.createContainer(), "Form One");
        }

        //init scheduler
        initScheduler();

        //display window
        window.open(display);
    }

    private void initHeadComponents() {
        linkFeedback.getButtonPressListeners().add(new ButtonPressListener() {
            public void buttonPressed(Button button) {
                try {
                    Desktop.getDesktop().mail(new URI("mailto:dmitry.s.mamonov@gmail.com?subject=WtkxBrowser:%20"));
                    Prompt.prompt("Email composer should be openned now.", window);
                } catch (Exception ex) {
                    try {
                        Desktop.getDesktop().browse(new URI("https://code.google.com/p/wtkxbrowser/issues/entry"));
                    } catch (Exception ex2) {
                        Prompt.prompt(ex.getMessage(), window);
                    }
                }
            }
        });

        listViewOfWtkxResources.setItemRenderer(new ListViewItemRenderer() {
            @Override
            public void render(Object item, int index, ListView listView, boolean selected, boolean checked, boolean highlighted, boolean disabled) {
                Object displayItem = index == 0 ? (item + " (displaying)") : item + " (polling)";
                super.render(displayItem, index, listView, selected, checked, highlighted, disabled);
            }
        });
        listViewOfWtkxResources.getComponentMouseButtonListeners().add(new ComponentMouseButtonListener.Adapter() {
            @Override
            public boolean mouseClick(Component component, Mouse.Button button, int x, int y, int count) {
                //ISSUE: next code should work any way, but I'm not sure if I'm user getItemAt correctly
                if (listViewOfWtkxResources.getItemAt(y) < 0) {
                    new AddWtkxResourceAction().perform();
                    return true;
                } else if (count > 1) {
                    new ActivateWtkxResourceAction().perform();
                    return true;
                }
                return false;
            }
        });
        listViewOfWtkxResources.getDecorators().add(new Decorator() {
            private ListView listView;
            private Graphics2D graphics;

            public Graphics2D prepare(Component component, Graphics2D graphics) {
                this.listView = (ListView) component;
                this.graphics = graphics;

                return graphics;

            }

            public void update() {
                if (listView.getListData().getLength()==0){
                    String prompt  = "Click here to add WTKX resources";
                    Font font = (Font) listView.getStyles().get("font");
                    graphics.setFont(font);
                    graphics.setColor(Color.GRAY);
                    FontMetrics fontMetrics = graphics.getFontMetrics();
                    int promptWidth = fontMetrics.stringWidth(prompt);
                    int promptHeight = fontMetrics.getHeight();
                    int wholeWidth = listView.getWidth();
                    graphics.drawString(prompt, (wholeWidth-promptWidth)/2, promptHeight);
                }
                listView = null;
                graphics = null;
            }

            public Bounds getBounds(Component component) {
                return component.getBounds();
            }

            public AffineTransform getTransform(Component component) {
                return new AffineTransform();
            }
        });

        buttonAddWtkx.setAction(new AddWtkxResourceAction());
        buttonRemoveWtkx.setAction(new RemoveWtkxResourceAction());
        buttonMoveWtkxUp.setAction(new ActivateWtkxResourceAction());
    }

    private void initBodyComponents() {
        buttonAddTab.setAction(new AddTabAction());
        buttonCloseTab.setAction(new RemoveTabAction());

        //ISSUE: how to add onClick listener for tabPage header?
        tabPane.getTabPaneSelectionListeners().add(new TabPaneSelectionListener.Adapter() {
            @Override
            public void selectedIndexChanged(TabPane tabPane, int previousSelectedIndex) {
                //save current state
                if (previousSelectedIndex >= 0) {
                    Component previousTab = tabPane.getTabs().get(previousSelectedIndex);
                    model.saveHeaderStateToComponent(previousTab);
                }
                //load selected state
                Component currentTab = tabPane.getSelectedTab();
                model.loadHeaderStateFromComponent(currentTab);
            }
        });
    }

    private void initScheduler() {
        ApplicationContext.scheduleRecurringCallback(new ResourcesChangesMonitorRunnable(), 250L);
    }

    private Component addTab(Container container, String name) {
        if (name == null || name.trim().isEmpty() == true) {
            name = "Form " + (tabPane.getTabs().getLength() + 1);
        }
        tabPane.getTabs().add(container);
        TabPane.setLabel(container, name);
        tabPane.setSelectedIndex(tabPane.getTabs().getLength() - 1);
        return container;
    }

    public boolean shutdown(boolean optional) throws Exception {
        model.save();
        if (window != null) {
            window.close();
        }
        return false; //shutdown will be fine.
    }

    public void suspend() throws Exception {
    }

    public void resume() throws Exception {
    }

    public static void main(String[] args) {
        System.setErr(new PrintStream(stderr));
        DesktopApplicationContext.main(WtkxBrowser.class, args);
    }

    private class Model {
        IdentityHashMap<Component, Map> userData = new IdentityHashMap();
        private static final String RESOURCES_LIST = "resourcesList";
        private static final String NAME = "name";

        public void saveHeaderStateToComponent(Component component) {
            if (component != null) {
                Map<String, Object> state = new HashMap<String, Object>();
                state.put(NAME, TabPane.getLabel(component));
                header.store(state);
                List<String> resourcesList = new ArrayList<String>();
                for (File file : (Iterable<File>) listViewOfWtkxResources.getListData()) {
                    resourcesList.add(file.getAbsolutePath());
                }
                state.put(RESOURCES_LIST, resourcesList);
                setComponentState(component, state);
            }
        }

        public void loadHeaderStateFromComponent(Component component) {
            Map<String, Object> state = getComponentState(component);
            header.load(state);
            List<File> resourcesList = new ArrayList<File>();
            Iterable<String> resourcesListSource = (Iterable<String>) state.get(RESOURCES_LIST);
            if (resourcesListSource != null) {
                for (String file : resourcesListSource) {
                    resourcesList.add(new File(file));
                }
            }
            listViewOfWtkxResources.setListData(resourcesList);
        }

        public Map<String, Object> getComponentState(Component component) {
            Map<String, Object> state = null;
            if (component != null) {
                Object compState = userData.get(component);
                if (compState instanceof Map) {
                    state = (Map<String, Object>) compState;
                }
            }
            if (state == null) {
                state = new HashMap<String, Object>();
            }
            state.put(NAME, TabPane.getLabel(component));
            return state;
        }

        public void setComponentState(Component component, Map<String, Object> state) {
            if (component != null) {
                userData.put(component, state);
            }
        }

        public List<Object> getStatesList() {
            List<Object> statesList = new ArrayList<Object>();
            for (Component tab : tabPane.getTabs()) {
                statesList.add(getComponentState(tab));
            }
            return statesList;
        }

        public Container createContainer() {
            BoxPane container = new BoxPane(Orientation.HORIZONTAL);
            //container.setStyles("{fill:true}");
            return container;
        }

        public void load() {
            try {
                List<Map> statesList = (List<Map>) JSONSerializer.parse(FileUtils.readFileToString(new File("WtkxBrowserSession.json"), "UTF-8"));
                //ISSUE: why can't I just call .clear() or some static method
                tabPane.getTabs().remove(0, tabPane.getTabs().getLength());

                for (Map state : statesList) {
                    String name = (String) state.get(NAME);
                    Container container = createContainer();
                    setComponentState(container, state);
                    addTab(container, name);
                }
                loadHeaderStateFromComponent(tabPane.getSelectedTab());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        public void save() {
            try {
                saveHeaderStateToComponent(tabPane.getSelectedTab());
                String json = JSONSerializer.toString(getStatesList());
                FileUtils.writeStringToFile(new File("WtkxBrowserSession.json"), json, "UTF-8");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        public void print() {
            for (Object obj : userData.values()) {
                try {
                    System.out.println("json = " + JSONSerializer.toString(obj));
                } catch (SerializationException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private class ResourcesChangesMonitorRunnable implements Runnable {
        Component recentTab = null;
        java.util.List<File> recentList = new java.util.ArrayList<File>();
        java.util.Map<File, byte[]> recentMap = new java.util.HashMap<File, byte[]>();

        @Override
        public void run() {
            //some extra work:
            //all logs printed to stderr is collected in "stderr" buffer,
            //to eliminate it overflowe it's better to reset it's content
            //as often as possible.
            stderr.reset();

            //setup actual component
            Component actualTab = tabPane.getSelectedTab();
            //read currently selected files state
            java.util.Map<File, byte[]> actualMap = new java.util.HashMap<File, byte[]>();
            //ISSUE: why I can't just copy data from pivot to java.util collection
            //ISSUE: or why can't I do containsAll and other checks on pivot collections
            java.util.List<File> actualList = new java.util.ArrayList<File>();
            for (File file : (List<File>) listViewOfWtkxResources.getListData()) {
                actualList.add(file);
            }

            //read actual data from disk (most probably from filesystem cache)
            for (File wtkx : actualList) {
                try {
                    actualMap.put(wtkx, FileUtils.readFileToByteArray(wtkx));
                } catch (IOException ex) {
                    actualMap.put(wtkx, new byte[0]);
                }
            }

            boolean changed = false;
            //check current tab is maybe changed
            if (recentTab != actualTab) {
                changed = true;
            }
            //check if files list not matches
            if (changed == false) {
                if (recentList.equals(actualList) == false) {
                    changed = true;
                }
            }
            if (changed == false) {
                //check is files content not matches
                for (File wtkx : actualList) {
                    byte[] left = recentMap.get(wtkx);
                    byte[] right = actualMap.get(wtkx);
                    if (Arrays.equals(left, right) == false) {
                        changed = true;
                        break;
                    }
                }
            }
            //if changes found
            if (changed == true) {
                recentTab = actualTab;
                recentList = actualList;
                recentMap = actualMap;
                reload(actualList);
            }
        }

        private void reload(java.util.List<File> wtkxsList) {
            try {
                if (wtkxsList.isEmpty() == false) {
                    File first = wtkxsList.get(0);
                    WTKXSerializer wtkxSerializer = new WTKXSerializer();
                    Component component = (Component) wtkxSerializer.readObject(first.toURI().toURL());
                    setupComponent(component, false);
                } else {
                    setupComponent(createTextArea("Please select WTKX resources in list above to display it content here", null), false);
                }
            } catch (Throwable th) {
                setupComponent(createTextArea(null, th), false);
            }
        }

        private void setupComponent(Component component, boolean recursion) {
            try {
                Container container = (Container) tabPane.getSelectedTab();
                if (container != null) {
                    container.removeAll();
                    container.add(component);
                }
            } catch (Throwable th) {
                if (recursion == true) {
                    th.printStackTrace();
                    Alert.alert(toString(th), window);
                } else {
                    setupComponent(createTextArea(null, th), true);
                }
            }
        }

        private Component createTextArea(String message, Throwable th) {
            TextArea textArea = new TextArea();
            StringBuilder text = new StringBuilder();

            if (message != null) {
                text.append(message).append("\n\n");
            }
            if (th != null) {
                //text.append(toString(th));
                String error = new String(stderr.toByteArray());
                text.append(error);
                if (error.contains(th.getMessage())==false){
                    text.append("\n\n").append(toString(th));
                }
                textArea.setStyles("{color:'#aa0000'}");
            }
            textArea.setText(text.toString());
            textArea.setEditable(false);
            return textArea;
        }

        private String toString(Throwable th) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            th.printStackTrace(pw);
            pw.flush();
            return sw.toString();
        }

    }

    private class AddWtkxResourceAction extends Action {
        @Override
        public void perform() {
            fileBrowserSheet.open(window, new SheetCloseListener() {
                @Override
                public void sheetClosed(Sheet sheet) {
                    if (sheet.getResult() == true) {
                        for (File file : new ArrayList<File>(fileBrowserSheet.getSelectedFiles())) {
                            List<Object> model = (List<Object>) listViewOfWtkxResources.getListData();
                            model.add(file.getAbsoluteFile());
                        }
                    }
                }
            });
        }
    }

    private class RemoveWtkxResourceAction extends Action {
        @Override
        public void perform() {
            List<Object> list = (List<Object>) listViewOfWtkxResources.getListData();
            List<Object> selected = new ArrayList(listViewOfWtkxResources.getSelectedItems());
            for (Object obj : selected) {
                list.remove(obj);
            }
        }
    }

    private class ActivateWtkxResourceAction extends Action {
        @Override
        public void perform() {
            List<Object> list = (List<Object>) listViewOfWtkxResources.getListData();
            List<Object> selected = new ArrayList(listViewOfWtkxResources.getSelectedItems());
            for (Object obj : selected) {
                list.remove(obj);
            }
            int index = 0;
            for (Object obj : selected) {
                list.insert(obj, index);
                index++;
            }
            listViewOfWtkxResources.scrollAreaToVisible(0, 0, 1, 1);
            listViewOfWtkxResources.setSelectedItems(selected);
        }
    }


    private class AddTabAction extends Action {
        @Override
        public void perform() {
            final TextInput textInput = new TextInput();
            Prompt p = new Prompt(MessageType.QUESTION, "Enter name for a new tab", new ArrayList<String>("OK"), textInput);
            p.open(window, new SheetCloseListener() {
                @Override
                public void sheetClosed(Sheet sheet) {
                    if (sheet.getResult() == true) {
                        addTab(model.createContainer(), textInput.getText());
                    }
                }
            });
        }
    }

    private class RemoveTabAction extends Action {
        @Override
        public void perform() {
            if (tabPane.getTabs().getLength() == 1) {
                Alert.alert("Can't close last tab", window);
                return;
            }
            try {
                int componentIndex = tabPane.getSelectedIndex();
                Component component = tabPane.getSelectedTab();
                if (component != null) {
                    tabPane.getTabs().remove(component);
                    int tabsCount = tabPane.getTabs().getLength();
                    if (tabsCount > 0) {
                        if (componentIndex >= tabsCount) {
                            tabPane.setSelectedIndex(tabsCount - 1);
                        } else {
                            tabPane.setSelectedIndex(componentIndex);
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
